Kuasai penanganan kesalahan TypeScript dengan pola keamanan tipe. Bangun aplikasi tangguh menggunakan kesalahan kustom, type guard, dan result monad.
Penanganan Kesalahan TypeScript: Pola Keamanan Tipe Pengecualian
Dalam dunia pengembangan perangkat lunak, di mana aplikasi menggerakkan segalanya mulai dari sistem keuangan global hingga interaksi seluler harian, membangun sistem yang tangguh dan toleran terhadap kesalahan bukan hanya praktik terbaik—ini adalah kebutuhan mendasar. Sementara JavaScript menawarkan lingkungan yang dinamis dan fleksibel, pengetikannya yang longgar terkadang dapat menyebabkan kejutan saat runtime, terutama ketika berurusan dengan kesalahan. Di sinilah TypeScript berperan, membawa pemeriksaan tipe statis ke garis depan dan menawarkan alat yang ampuh untuk meningkatkan prediktabilitas dan pemeliharaan kode.
Penanganan kesalahan adalah aspek penting dari aplikasi yang tangguh. Tanpa strategi yang jelas, masalah yang tidak terduga dapat menyebabkan perilaku yang tidak dapat diprediksi, kerusakan data, atau bahkan kegagalan sistem yang lengkap. Ketika dikombinasikan dengan keamanan tipe TypeScript, penanganan kesalahan bertransformasi dari tugas pengkodean defensif menjadi bagian yang terstruktur, dapat diprediksi, dan terkelola dari arsitektur aplikasi Anda.
Panduan komprehensif ini mendalami nuansa penanganan kesalahan TypeScript, mengeksplorasi berbagai pola dan praktik terbaik untuk memastikan keamanan tipe pengecualian. Kita akan melampaui blok try...catch dasar, mengungkap cara memanfaatkan fitur TypeScript untuk mendefinisikan, menangkap, dan menangani kesalahan dengan presisi yang tak tertandingi. Apakah Anda sedang membangun aplikasi perusahaan yang kompleks, layanan web ber traffic tinggi, atau pengalaman frontend mutakhir, memahami pola-pola ini akan memberdayakan Anda untuk menulis kode yang lebih andal, dapat di-debug, dan mudah dipelihara untuk audiens global pengembang dan pengguna.
Fondasi: Objek Kesalahan JavaScript dan try...catch
Sebelum kita menjelajahi peningkatan TypeScript, penting untuk memahami dasar-dasar penanganan kesalahan di JavaScript. Mekanisme inti adalah objek Error, yang berfungsi sebagai dasar untuk semua kesalahan bawaan standar.
Tipe Kesalahan Standar di JavaScript
Error: Objek kesalahan dasar generik. Sebagian besar kesalahan kustom memperluas ini.TypeError: Menunjukkan operasi dilakukan pada nilai tipe yang salah.ReferenceError: Dilemparkan ketika referensi yang tidak valid dibuat (misalnya, mencoba menggunakan variabel yang tidak dideklarasikan).RangeError: Menunjukkan variabel atau parameter numerik berada di luar rentang validnya.SyntaxError: Terjadi saat mengurai kode yang bukan JavaScript yang valid.URIError: Dilemparkan ketika fungsi sepertiencodeURI()ataudecodeURI()digunakan secara tidak benar.EvalError: Berkaitan dengan fungsi globaleval()(kurang umum dalam kode modern).
Blok try...catch Dasar
Cara mendasar untuk menangani kesalahan sinkron di JavaScript (dan TypeScript) adalah dengan pernyataan try...catch:
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Division by zero is not allowed.");
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(`Result: ${result}`);
} catch (error) {
console.error("An error occurred:", error);
}
// Output:
// An error occurred: Error: Division by zero is not allowed.
Dalam JavaScript tradisional, parameter blok catch secara implisit bertipe any. Ini berarti Anda dapat memperlakukan error sebagai apa saja, yang menyebabkan potensi masalah runtime jika Anda mengharapkan bentuk kesalahan tertentu tetapi menerima sesuatu yang lain (misalnya, string sederhana atau angka yang dilemparkan). Kurangnya keamanan tipe ini dapat membuat penanganan kesalahan menjadi rapuh dan sulit untuk di-debug.
Evolusi TypeScript: Tipe unknown dalam Klausa Catch
Dengan diperkenalkannya TypeScript 4.4, tipe variabel klausa catch diubah dari any menjadi unknown. Ini adalah peningkatan signifikan untuk keamanan tipe. Tipe unknown memaksa pengembang untuk secara eksplisit mempersempit tipe kesalahan sebelum beroperasi padanya. Ini berarti Anda tidak dapat begitu saja mengakses properti seperti error.message atau error.statusCode tanpa terlebih dahulu menegaskan atau memeriksa tipe error. Perubahan ini mencerminkan komitmen terhadap jaminan tipe yang lebih kuat, mencegah jebakan umum di mana pengembang salah mengasumsikan bentuk kesalahan.
try {
throw "Oops, something went wrong!"; // Throwing a string, which is valid in JS
} catch (error) {
// In TS 4.4+, 'error' is of type 'unknown'
// console.log(error.message); // ERROR: 'error' is of type 'unknown'.
}
Ketelitian ini adalah fitur, bukan bug. Ini memaksa kita untuk menulis logika penanganan kesalahan yang lebih tangguh, meletakkan dasar untuk pola yang aman tipe yang akan kita jelajahi selanjutnya.
Mengapa Keamanan Tipe dalam Kesalahan Sangat Penting untuk Aplikasi Global
Untuk aplikasi yang melayani basis pengguna global dan dikembangkan oleh tim internasional, penanganan kesalahan yang konsisten dan dapat diprediksi sangat penting. Keamanan tipe dalam kesalahan menawarkan beberapa keuntungan yang berbeda:
- Peningkatan Keandalan dan Stabilitas: Dengan secara eksplisit mendefinisikan tipe kesalahan, Anda mencegah crash runtime tak terduga yang dapat timbul dari upaya mengakses properti yang tidak ada pada objek kesalahan yang cacat. Ini menghasilkan aplikasi yang lebih stabil, kritis untuk layanan di mana downtime dapat memiliki biaya finansial atau reputasi yang signifikan di berbagai pasar.
- Peningkatan Pengalaman Pengembang (DX) dan Pemeliharaan: Ketika pengembang dengan jelas memahami kesalahan apa yang dapat dilemparkan atau dikembalikan oleh suatu fungsi, mereka dapat menulis logika penanganan yang lebih terarah dan efektif. Ini mengurangi beban kognitif, mempercepat pengembangan, dan membuat kode lebih mudah dipelihara dan direfaktor, terutama dalam tim besar yang terdistribusi yang mencakup zona waktu dan latar belakang budaya yang berbeda.
- Logika Penanganan Kesalahan yang Dapat Diprediksi: Kesalahan yang aman tipe memungkinkan pemeriksaan yang lengkap. Anda dapat menulis pernyataan
switchatau rantaiif/else ifyang mencakup semua kemungkinan tipe kesalahan, memastikan tidak ada kesalahan yang tidak tertangani. Prediktabilitas ini sangat penting untuk sistem yang harus mematuhi perjanjian tingkat layanan (SLA) yang ketat atau standar kepatuhan peraturan di seluruh dunia. - Debugging dan Pemecahan Masalah yang Lebih Baik: Tipe kesalahan spesifik dengan metadata yang kaya memberikan konteks yang sangat berharga selama debugging. Alih-alih "sesuatu yang salah" generik, Anda mendapatkan informasi yang tepat seperti
NetworkErrordenganstatusCode: 503, atauValidationErrordengan daftar bidang yang tidak valid. Kejelasan ini secara drastis mengurangi waktu yang dihabiskan untuk mendiagnosis masalah, manfaat besar bagi tim operasi yang bekerja di berbagai lokasi geografis. - Kontrak API yang Jelas: Saat merancang API atau modul yang dapat digunakan kembali, secara eksplisit menyatakan tipe kesalahan yang dapat dilemparkan menjadi bagian dari kontrak fungsi. Ini meningkatkan titik integrasi, memungkinkan layanan atau tim lain untuk berinteraksi dengan kode Anda secara lebih prediktif dan aman.
- Memfasilitasi Internasionalisasi Pesan Kesalahan: Dengan tipe kesalahan yang terdefinisi dengan baik, Anda dapat memetakan kode kesalahan tertentu ke pesan yang dilokalkan untuk pengguna dalam bahasa dan budaya yang berbeda.
UserNotFoundErrordapat menampilkan "User not found" dalam bahasa Inggris, "Utilisateur introuvable" dalam bahasa Prancis, atau "Usuario no encontrado" dalam bahasa Spanyol, meningkatkan pengalaman pengguna secara global tanpa mengubah logika penanganan kesalahan yang mendasarinya.
Menerapkan keamanan tipe dalam penanganan kesalahan adalah investasi untuk masa depan aplikasi Anda, memastikan aplikasi tersebut tetap tangguh, dapat diskalakan, dan mudah dikelola seiring evolusinya dan melayani audiens global.
Pola 1: Pemeriksaan Tipe Runtime (Mempersempit Kesalahan unknown)
Mengingat bahwa variabel blok catch bertipe unknown di TypeScript 4.4+, pola pertama dan paling mendasar adalah mempersempit tipe kesalahan di dalam blok catch. Ini memastikan Anda hanya mengakses properti yang dijamin ada pada objek kesalahan setelah pemeriksaan.
Menggunakan instanceof Error
Cara paling umum dan mudah untuk mempersempit kesalahan unknown adalah dengan memeriksa apakah itu instance dari kelas Error bawaan (atau salah satu kelas turunannya seperti TypeError, ReferenceError, dll.).
function riskyOperation(): void {
// Simulate different types of errors
const rand = Math.random();
if (rand < 0.3) {
throw new Error("Generic error occurred!");
} else if (rand < 0.6) {
throw new TypeError("Invalid data type provided.");
} else {
throw { code: 500, message: "Internal Server Error" }; // Non-Error object
}
}
try {
riskyOperation();
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`Caught an Error object: ${error.message}`);
// You can also check for specific Error subclasses
if (error instanceof TypeError) {
console.error("Specifically, a TypeError was caught.");
}
} else if (typeof error === 'string') {
console.error(`Caught a string error: ${error}`);
} else if (typeof error === 'object' && error !== null && 'message' in error) {
// Handle custom objects that have a 'message' property
console.error(`Caught a custom error object with message: ${(error as { message: string }).message}`);
} else {
console.error("An unexpected type of error occurred:", error);
}
}
Pendekatan ini memberikan keamanan tipe dasar, memungkinkan Anda mengakses properti message dan name dari objek Error standar. Namun, untuk skenario kesalahan yang lebih spesifik, Anda akan menginginkan informasi yang lebih kaya.
Type Guard Kustom untuk Objek Kesalahan Spesifik
Seringkali, aplikasi Anda akan mendefinisikan struktur kesalahan kustomnya sendiri, mungkin berisi kode kesalahan spesifik, pengenal unik, atau metadata tambahan. Untuk mengakses properti kustom ini dengan aman, Anda dapat membuat type guard yang ditentukan pengguna.
// 1. Define custom error interfaces/types
interface NetworkError {
name: "NetworkError";
message: string;
statusCode: number;
url: string;
}
interface ValidationError {
name: "ValidationError";
message: string;
fields: { [key: string]: string };
}
// 2. Create type guards for each custom error
function isNetworkError(error: unknown): error is NetworkError {
return (
typeof error === 'object' &&
error !== null &&
'name' in error &&
(error as { name: string }).name === "NetworkError" &&
'message' in error &&
'statusCode' in error &&
'url' in error
);
}
function isValidationError(error: unknown): error is ValidationError {
return (
typeof error === 'object' &&
error !== null &&
'name' in error &&
(error as { name: string }).name === "ValidationError" &&
'message' in error &&
'fields' in error &&
typeof (error as { fields: unknown }).fields === 'object'
);
}
// 3. Example usage in a 'try...catch' block
function fetchData(url: string): Promise<any> {
return new Promise((resolve, reject) => {
// Simulate an API call that might throw different errors
const rand = Math.random();
if (rand < 0.4) {
reject(new Error("Something unexpected happened."));
} else if (rand < 0.7) {
reject({
name: "NetworkError",
message: "Failed to fetch data",
statusCode: 503,
url
} as NetworkError);
} else {
reject({
name: "ValidationError",
message: "Invalid input data",
fields: { 'email': 'Invalid format' }
} as ValidationError);
}
});
}
async function processData() {
const url = "https://api.example.com/data";
try {
const data = await fetchData(url);
console.log("Data fetched successfully:", data);
} catch (error: unknown) {
if (isNetworkError(error)) {
console.error(`Network Error from ${error.url}: ${error.message} (Status: ${error.statusCode})`);
// Specific handling for network issues, e.g., retry logic or user notification
} else if (isValidationError(error)) {
console.error(`Validation Error: ${error.message}`);
console.error("Invalid fields:", error.fields);
// Specific handling for validation errors, e.g., display errors next to form fields
} else if (error instanceof Error) {
console.error(`Standard Error: ${error.message}`);
} else {
console.error("An unknown or unexpected error type occurred:", error);
// Fallback for truly unexpected errors
}
}
}
processData();
Pola ini membuat logika penanganan kesalahan Anda jauh lebih tangguh dan mudah dibaca. Ini memaksa Anda untuk mempertimbangkan dan menangani secara eksplisit berbagai skenario kesalahan, yang sangat penting untuk membangun aplikasi yang mudah dipelihara.
Pola 2: Kelas Kesalahan Kustom
Meskipun type guard pada antarmuka berguna, pendekatan yang lebih terstruktur dan berorientasi objek adalah mendefinisikan kelas kesalahan kustom. Pola ini memungkinkan Anda memanfaatkan pewarisan, menciptakan hierarki tipe kesalahan spesifik yang dapat ditangkap dan ditangani dengan presisi menggunakan pemeriksaan instanceof, mirip dengan kesalahan bawaan JavaScript tetapi dengan properti kustom Anda sendiri.
Memperluas Kelas Error Bawaan
Praktik terbaik untuk kesalahan kustom di TypeScript (dan JavaScript) adalah memperluas kelas Error dasar. Ini memastikan bahwa kesalahan kustom Anda mempertahankan properti seperti message dan stack, yang sangat penting untuk debugging dan logging.
// Base Custom Error
class CustomApplicationError extends Error {
constructor(message: string, public code: string = 'GENERIC_ERROR') {
super(message);
this.name = this.constructor.name; // Sets the error name to the class name
// Preserve stack trace for better debugging
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
// Specific Custom Errors
class DatabaseConnectionError extends CustomApplicationError {
constructor(message: string, public databaseName: string, public connectionString?: string) {
super(message, 'DB_CONN_ERROR');
}
}
class UserAuthenticationError extends CustomApplicationError {
constructor(message: string, public userId?: string, public reason: 'INVALID_CREDENTIALS' | 'SESSION_EXPIRED' | 'FORBIDDEN' = 'INVALID_CREDENTIALS') {
super(message, 'AUTH_ERROR');
}
}
class DataValidationFailedError extends CustomApplicationError {
constructor(message: string, public invalidFields: { [key: string]: string }) {
super(message, 'VALIDATION_ERROR');
}
}
Keuntungan Kelas Kesalahan Kustom
- Makna Semantik: Nama kelas kesalahan memberikan wawasan langsung tentang sifat masalah (misalnya,
DatabaseConnectionErrordengan jelas menunjukkan masalah database). - Ekstensibilitas: Anda dapat menambahkan properti spesifik ke setiap tipe kesalahan (misalnya,
statusCode,userId,fields) yang relevan dengan konteks kesalahan tertentu itu, memperkaya informasi kesalahan untuk debugging dan penanganan. - Identifikasi Mudah dengan
instanceof: Menangkap dan membedakan antara kesalahan kustom yang berbeda menjadi sepele menggunakaninstanceof, memungkinkan logika penanganan kesalahan yang presisi. - Pemeliharaan: Memusatkan definisi kesalahan membuat basis kode Anda lebih mudah dipahami dan dikelola. Jika properti kesalahan berubah, Anda memperbarui satu definisi kelas.
- Dukungan Alat: IDE dan linter sering kali dapat memberikan saran dan peringatan yang lebih baik saat berurusan dengan kelas kesalahan yang berbeda.
Menangani Kelas Kesalahan Kustom
function performDatabaseOperation(query: string): any {
const rand = Math.random();
if (rand < 0.4) {
throw new DatabaseConnectionError("Failed to connect to primary DB", "users_db");
} else if (rand < 0.7) {
throw new UserAuthenticationError("User session expired", "user123", 'SESSION_EXPIRED');
} else {
throw new DataValidationFailedError("User input invalid", { 'name': 'Name is too short', 'email': 'Invalid email format' });
}
}
try {
performDatabaseOperation("SELECT * FROM users");
} catch (error: unknown) {
if (error instanceof DatabaseConnectionError) {
console.error(`Database Error: ${error.message}. DB: ${error.databaseName}. Code: ${error.code}`);
// Logic to attempt reconnect or notify ops team
} else if (error instanceof UserAuthenticationError) {
console.warn(`Authentication Error (${error.reason}): ${error.message}. User ID: ${error.userId || 'N/A'}`);
// Logic to redirect to login page or refresh token
} else if (error instanceof DataValidationFailedError) {
console.error(`Validation Error: ${error.message}. Invalid fields: ${JSON.stringify(error.invalidFields)}`);
// Logic to display validation messages to the user
} else if (error instanceof Error) {
console.error(`An unexpected standard error occurred: ${error.message}`);
} else {
console.error("A truly unexpected error occurred:", error);
}
}
Menggunakan kelas kesalahan kustom secara signifikan meningkatkan kualitas penanganan kesalahan Anda. Ini memungkinkan Anda membangun sistem manajemen kesalahan yang canggih yang tangguh dan mudah dipahami, yang sangat berharga untuk aplikasi skala besar dengan logika bisnis yang kompleks.
Pola 3: Pola Monad Result/Either (Penanganan Kesalahan Eksplisit)
Meskipun try...catch dengan kelas kesalahan kustom memberikan penanganan yang tangguh untuk pengecualian, beberapa paradigma pemrograman fungsional berpendapat bahwa pengecualian memecah aliran kontrol normal dan dapat membuat kode lebih sulit dipahami, terutama ketika berurusan dengan operasi asinkron. Pola "Result" atau "Either" monad menawarkan alternatif dengan membuat keberhasilan dan kegagalan eksplisit dalam tipe kembalian fungsi, memaksa pemanggil untuk menangani kedua hasil tanpa mengandalkan `try/catch` untuk aliran kontrol.
Apa itu Pola Result/Either?
Alih-alih melempar kesalahan, fungsi yang mungkin gagal mengembalikan tipe khusus (sering disebut Result atau Either) yang mengenkapsulasi nilai yang berhasil (Ok atau Right) atau kesalahan (Err atau Left). Pola ini umum di bahasa seperti Rust (Result<T, E>) dan Scala (Either<L, R>).
Ide intinya adalah bahwa tipe pengembalian itu sendiri memberi tahu Anda bahwa fungsi tersebut memiliki dua kemungkinan hasil, dan sistem tipe TypeScript memastikan Anda menangani keduanya.
Mengimplementasikan Tipe Result Sederhana
type Result<T, E> = { success: true; value: T } | { success: false; error: E };
// Helper functions to create Ok and Err results
const ok = <T, E>(value: T): Result<T, E> => ({ success: true, value });
const err = <T, E>(error: E): Result<T, E> => ({ success: false, error });
interface User {
id: string;
name: string;
email: string;
}
// Custom errors for this pattern (can still use classes)
class UserNotFoundError extends Error {
constructor(userId: string) {
super(`User with ID '${userId}' not found.`);
this.name = 'UserNotFoundError';
}
}
class DatabaseReadError extends Error {
constructor(message: string, public details?: string) {
super(message);
this.name = 'DatabaseReadError';
}
}
// Function that returns a Result type
function getUserById(id: string): Result<User, UserNotFoundError | DatabaseReadError> {
// Simulate database operation
const rand = Math.random();
if (rand < 0.3) {
return err(new UserNotFoundError(id)); // Return an error result
} else if (rand < 0.6) {
return err(new DatabaseReadError("Failed to read from DB", "Connection timed out")); // Return a database error
} else {
return ok({
id: id,
name: "John Doe",
email: `john.${id}@example.com`
}); // Return a success result
}
}
// Consuming the Result type
const userResult = getUserById("user-123");
if (userResult.success) {
console.log(`User found: ${userResult.value.name}, Email: ${userResult.value.email}`);
} else {
// TypeScript knows userResult.error is of type UserNotFoundError | DatabaseReadError
if (userResult.error instanceof UserNotFoundError) {
console.error(`Application Error: ${userResult.error.message}`);
// Logic for user not found, e.g., display a message to the user
} else if (userResult.error instanceof DatabaseReadError) {
console.error(`System Error: ${userResult.error.message}. Details: ${userResult.error.details}`);
// Logic for database issue, e.g., retry or alert system administrators
} else {
// Exhaustive check or fallback for other potential errors
console.error("An unexpected error occurred:", userResult.error);
}
}
Pola ini bisa sangat kuat ketika merangkai operasi yang mungkin gagal, karena Anda dapat menggunakan map, flatMap (atau andThen), dan konstruksi fungsional lainnya untuk memproses Result tanpa pemeriksaan if/else eksplisit di setiap langkah, menunda penanganan kesalahan ke satu titik.
Keuntungan Pola Result
- Penanganan Kesalahan Eksplisit: Fungsi secara eksplisit mendeklarasikan kesalahan apa yang dapat mereka kembalikan dalam tanda tangan tipenya, memaksa pemanggil untuk mengakui dan menangani semua kemungkinan keadaan kegagalan. Ini menghilangkan pengecualian yang "terlupakan".
- Transparansi Referensial: Dengan menghindari pengecualian sebagai mekanisme aliran kontrol, fungsi menjadi lebih prediktif dan lebih mudah diuji.
- Keterbacaan yang Ditingkatkan: Jalur kode untuk keberhasilan dan kegagalan dibedakan dengan jelas, membuatnya lebih mudah untuk mengikuti logika.
- Komposabilitas: Tipe Hasil tersusun dengan baik dengan teknik pemrograman fungsional, memungkinkan propagasi dan transformasi kesalahan yang elegan.
- Tidak Perlu Boilerplate
try...catch: Dalam banyak skenario, pola ini dapat mengurangi kebutuhan akan bloktry...catch, terutama ketika menyusun banyak operasi yang rentan.
Pertimbangan dan Trade-off
- Kekurangan Kata: Bisa lebih bertele-tele untuk operasi sederhana atau ketika tidak memanfaatkan konstruksi fungsional secara efektif.
- Kurva Belajar: Pengembang yang baru mengenal pemrograman fungsional atau monad mungkin awalnya menganggap pola ini kompleks.
- Operasi Asinkron: Meskipun berlaku, integrasi dengan kode asinkron berbasis Promise yang ada memerlukan pembungkusan atau transformasi yang cermat. Pustaka seperti
neverthrowataufp-tsmenyediakan implementasi `Either`/`Result` yang lebih canggih yang disesuaikan untuk TypeScript, seringkali dengan dukungan async yang lebih baik.
Pola Result/Either adalah pilihan yang sangat baik untuk aplikasi yang memprioritaskan penanganan kesalahan eksplisit, kemurnian fungsional, dan penekanan kuat pada keamanan tipe di semua jalur eksekusi. Sangat cocok untuk sistem yang kritis terhadap misi di mana setiap kemungkinan kegagalan harus diperhitungkan secara eksplisit.
Pola 4: Strategi Penanganan Kesalahan Terpusat
Meskipun blok `try...catch` individual dan tipe Result menangani kesalahan lokal, aplikasi yang lebih besar, terutama yang melayani basis pengguna global, sangat diuntungkan dari strategi penanganan kesalahan terpusat. Strategi ini memastikan pelaporan, logging, dan umpan balik pengguna yang konsisten di seluruh sistem, terlepas dari di mana kesalahan berasal.
Penangan Kesalahan Global
Memusatkan penanganan kesalahan memungkinkan Anda untuk:
- Mencatat kesalahan secara konsisten ke sistem pemantauan (misalnya, Sentry, Datadog).
- Memberikan pesan kesalahan generik yang ramah pengguna untuk kesalahan yang tidak diketahui.
- Menangani kekhawatiran di seluruh aplikasi seperti mengirim notifikasi, membatalkan transaksi, atau memicu circuit breaker.
- Memastikan PII (Informasi Identitas Pribadi) atau data sensitif tidak terekspos dalam pesan kesalahan kepada pengguna atau log yang melanggar peraturan privasi data (misalnya, GDPR, CCPA).
Backend (Node.js/Express) Contoh
Dalam aplikasi Node.js Express, Anda dapat mendefinisikan middleware penanganan kesalahan yang menangkap semua kesalahan yang dilemparkan oleh rute dan middleware Anda lainnya. Middleware ini harus menjadi yang terakhir didaftarkan.
import express, { Request, Response, NextFunction } from 'express';
// Assume these are our custom error classes
class APIError extends Error {
constructor(message: string, public statusCode: number = 500) {
super(message);
this.name = 'APIError';
}
}
class UnauthorizedError extends APIError {
constructor(message: string = 'Unauthorized') {
super(message, 401);
this.name = 'UnauthorizedError';
}
}
class BadRequestError extends APIError {
constructor(message: string = 'Bad Request') {
super(message, 400);
this.name = 'BadRequestError';
}
}
const app = express();
app.get('/api/users/:id', (req: Request, res: Response, next: NextFunction) => {
const userId = req.params.id;
if (userId === 'admin') {
return next(new UnauthorizedError('Access denied for admin user.'));
}
if (!/^[a-z0-9]+$/.test(userId)) {
return next(new BadRequestError('Invalid user ID format.'));
}
// Simulate a successful operation or another unexpected error
const rand = Math.random();
if (rand < 0.5) {
// Successfully fetch user
res.json({ id: userId, name: 'Test User' });
} else {
// Simulate an unexpected internal error
next(new Error('Failed to retrieve user data due to an unexpected issue.'));
}
});
// Type-safe error handling middleware
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
// Log the error for internal monitoring
console.error(`[ERROR] ${new Date().toISOString()} - ${req.method} ${req.originalUrl} -`, err);
if (err instanceof APIError) {
// Specific handling for known API errors
return res.status(err.statusCode).json({
status: 'error',
message: err.message,
code: err.name // Or a specific application-defined error code
});
} else if (err instanceof Error) {
// Generic handling for unexpected standard errors
return res.status(500).json({
status: 'error',
message: 'An unexpected server error occurred.',
// In production, avoid exposing detailed internal error messages to clients
detail: process.env.NODE_ENV === 'development' ? err.message : undefined
});
} else {
// Fallback for truly unknown error types
return res.status(500).json({
status: 'error',
message: 'An unknown server error occurred.',
detail: process.env.NODE_ENV === 'development' ? String(err) : undefined
});
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// Example cURL commands:
// curl http://localhost:3000/api/users/admin
// curl http://localhost:3000/api/users/invalid-id!
// curl http://localhost:3000/api/users/valid-id
Frontend (React) Contoh: Error Boundaries
Dalam framework frontend seperti React, Error Boundaries menyediakan cara untuk menangkap kesalahan JavaScript di mana pun dalam pohon komponen anaknya, mencatat kesalahan tersebut, dan menampilkan UI fallback alih-alih merusak seluruh aplikasi. TypeScript membantu mendefinisikan props dan state untuk batas-batas ini dan memeriksa tipe objek kesalahan.
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode; // Optional custom fallback UI
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
class AppErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
public state: ErrorBoundaryState = {
hasError: false,
error: null,
errorInfo: null,
};
// This static method is called after an error has been thrown by a descendant component.
static getDerivedStateFromError(_: Error): ErrorBoundaryState {
// Update state so the next render will show the fallback UI.
return { hasError: true, error: _, errorInfo: null };
}
// This method is called after an error has been thrown by a descendant component.
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// You can also log the error to an error reporting service here
console.error("Uncaught error in AppErrorBoundary:", error, errorInfo);
this.setState({ errorInfo: errorInfo, error: error });
}
public render() {
if (this.state.hasError) {
// You can render any custom fallback UI
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div style={{ padding: '20px', border: '1px solid red', borderRadius: '5px' }}>
<h2>Oops! Something went wrong.</h2>
<p>We're sorry for the inconvenience. Please try refreshing the page or contact support.</p>
{this.state.error && (
<details style={{ whiteSpace: 'pre-wrap', color: '#666' }}>
<summary>Error Details</summary>
<p>{this.state.error.message}</p>
{this.state.errorInfo && (
<p>Component Stack:<br/>{this.state.errorInfo.componentStack}</p>
)}
</details>
)}
</div>
);
}
return this.props.children;
}
}
// How to use it:
// function App() {
// return (
// <AppErrorBoundary>
// <SomePotentiallyFailingComponent />
// </AppErrorBoundary>
// );
// }
Membedakan Antara Kesalahan Operasional dan Kesalahan Pemrogram
Aspek penting dari penanganan kesalahan terpusat adalah membedakan antara dua kategori utama kesalahan:
- Kesalahan Operasional: Ini adalah masalah yang dapat diprediksi yang dapat terjadi selama operasi normal, seringkali di luar logika inti aplikasi. Contoh termasuk timeout jaringan, kegagalan koneksi database, input pengguna yang tidak valid, file tidak ditemukan, atau batas tarif. Kesalahan ini harus ditangani dengan anggun, seringkali menghasilkan pesan yang ramah pengguna atau logika coba lagi tertentu. Mereka biasanya tidak menunjukkan bug dalam kode Anda. Kelas kesalahan kustom dengan kode kesalahan spesifik sangat baik untuk ini.
- Kesalahan Pemrogram: Ini adalah bug dalam kode Anda. Contoh termasuk
ReferenceError(menggunakan variabel yang tidak terdefinisi),TypeError(memanggil metode padanull), atau kesalahan logika yang menyebabkan keadaan tak terduga. Ini umumnya tidak dapat dipulihkan saat runtime dan memerlukan perbaikan kode. Penangan kesalahan global harus mencatat ini secara ekstensif dan berpotensi memicu restart aplikasi atau peringatan ke tim pengembangan.
Dengan mengkategorikan kesalahan, penangan terpusat Anda dapat memutuskan apakah akan menampilkan pesan kesalahan generik, mencoba pemulihan, atau meningkatkan masalah ke pengembang. Perbedaan ini sangat penting untuk memelihara aplikasi yang sehat dan responsif di berbagai lingkungan.
Praktik Terbaik untuk Penanganan Kesalahan yang Aman Tipe
Untuk memaksimalkan manfaat TypeScript dalam strategi penanganan kesalahan Anda, pertimbangkan praktik terbaik ini:
- Selalu Persempit
unknowndi Blokcatch: Sejak TypeScript 4.4+, variabelcatchadalahunknown. Selalu lakukan pemeriksaan tipe runtime (misalnya,instanceof Error, type guard kustom) untuk mengakses properti kesalahan dengan aman. Ini mencegah kesalahan runtime umum. - Rancang Kelas Kesalahan Kustom yang Bermakna: Perluas kelas
Errordasar untuk membuat tipe kesalahan spesifik yang kaya secara semantik. Sertakan properti spesifik yang relevan dengan konteks (misalnya,statusCode,errorCode,invalidFields,userId) untuk membantu debugging dan penanganan. - Jelas Tentang Kontrak Kesalahan: Dokumentasikan kesalahan apa yang dapat dilemparkan atau dikembalikan oleh suatu fungsi. Jika menggunakan pola Result, ini diberlakukan oleh tanda tangan tipe pengembalian. Untuk `try/catch`, komentar JSDoc yang jelas atau tanda tangan fungsi yang menyampaikan pengecualian potensial sangat berharga.
- Catat Kesalahan Secara Komprehensif: Gunakan pendekatan logging terstruktur. Tangkap jejak tumpukan kesalahan lengkap, bersama dengan properti kesalahan kustom apa pun dan informasi kontekstual (misalnya, ID permintaan, ID pengguna, stempel waktu, lingkungan). Untuk aplikasi kritis, integrasikan dengan sistem logging dan pemantauan terpusat (misalnya, ELK Stack, Splunk, DataDog, Sentry).
- Hindari Melemparkan Tipe
stringatauobjectGenerik: Meskipun JavaScript mengizinkannya, melempar string mentah, angka, atau objek biasa membuat penanganan kesalahan yang aman tipe menjadi tidak mungkin dan mengarah pada kode yang rapuh. Selalu lempar instanceErroratau kelas kesalahan kustom. - Manfaatkan
neveruntuk Pemeriksaan Komprehensif: Saat berurusan dengan gabungan tipe kesalahan kustom (misalnya, dalam pernyataanswitchatau serangkaianif/else if), gunakan type guard yang mengarah ke tipe `never` untuk blokelseterakhir. Ini memastikan bahwa jika tipe kesalahan baru diperkenalkan, TypeScript akan menandai kasus yang tidak tertangani. - Terjemahkan Kesalahan untuk Pengalaman Pengguna: Pesan kesalahan internal adalah untuk pengembang. Untuk pengguna akhir, terjemahkan kesalahan teknis menjadi pesan yang jelas, dapat ditindaklanjuti, dan sesuai secara budaya. Pertimbangkan untuk menggunakan kode kesalahan yang memetakan ke pesan yang dilokalkan untuk mendukung internasionalisasi.
- Bedakan Antara Kesalahan yang Dapat Dipulihkan dan Tidak Dapat Dipulihkan: Rancang logika penanganan kesalahan Anda untuk membedakan antara kesalahan yang dapat dicoba ulang atau dikoreksi sendiri (misalnya, masalah jaringan) dan kesalahan yang menunjukkan cacat aplikasi fatal (misalnya, kesalahan pemrogram yang tidak tertangani).
- Uji Jalur Kesalahan Anda: Sama seperti Anda menguji jalur bahagia, uji jalur kesalahan Anda secara ketat. Pastikan aplikasi Anda menangani semua kondisi kesalahan yang diharapkan dengan baik dan gagal secara prediktif ketika terjadi kesalahan yang tidak terduga.
type SpecificError = DatabaseConnectionError | UserAuthenticationError | DataValidationFailedError;
function handleSpecificError(error: SpecificError) {
if (error instanceof DatabaseConnectionError) {
// ...
} else if (error instanceof UserAuthenticationError) {
// ...
} else if (error instanceof DataValidationFailedError) {
// ...
} else {
// This line should ideally be unreachable. If it is, a new error type was added
// to SpecificError but not handled here, causing a TS error.
const exhaustiveCheck: never = error; // TypeScript will flag this if 'error' is not 'never'
}
}
Mematuhi praktik-praktik ini akan meningkatkan aplikasi TypeScript Anda dari sekadar fungsional menjadi tangguh, andal, dan sangat mudah dipelihara, mampu melayani basis pengguna yang beragam di seluruh dunia.
Kesalahan Umum dan Cara Menghindarinya
Bahkan dengan niat terbaik, pengembang dapat terjebak dalam perangkap umum saat menangani kesalahan di TypeScript. Mengetahui jebakan ini dapat membantu Anda menghindarinya.
- Mengabaikan Tipe
unknowndi Blokcatch:Jebakan: Langsung mengasumsikan tipe
errordi blokcatchtanpa mempersempit.try { throw new Error("Oops"); } catch (error) { // Type 'unknown' is not assignable to type 'Error'. // Property 'message' does not exist on type 'unknown'. // console.error(error.message); // This will be a TypeScript error! }Penghindaran: Selalu gunakan
instanceof Erroratau type guard kustom untuk mempersempit tipe.try { throw new Error("Oops"); } catch (error: unknown) { if (error instanceof Error) { console.error(error.message); } else { console.error("A non-Error type was thrown:", error); } } - Generalisasi Blok
catchBerlebihan:Jebakan: Menangkap
Errorketika Anda hanya bermaksud menangani kesalahan kustom tertentu. Ini dapat menutupi masalah yang mendasarinya.// Assume a custom APIError class APIError extends Error { /* ... */ } function fetchData() { throw new APIError("Failed to fetch"); } function processData() { try { fetchData(); } catch (error: unknown) { // This catches APIError, but also *any* other Error that might be thrown // by fetchData or other code in the try block, potentially masking bugs. if (error instanceof Error) { console.error("Caught a generic error:", error.message); } } }Penghindaran: Jadilah sespesifik mungkin. Jika Anda mengharapkan kesalahan kustom tertentu, tangkap yang itu terlebih dahulu. Gunakan fallback untuk
Errorgenerik atauunknown.try { fetchData(); } catch (error: unknown) { if (error instanceof APIError) { // Handle APIError specifically console.error("API Error:", error.message); } else if (error instanceof Error) { // Handle other standard errors console.error("Unexpected standard Error:", error.message); } else { // Handle truly unknown errors console.error("Truly unexpected error:", error); } } - Kurangnya Pesan Kesalahan dan Konteks Spesifik:
Jebakan: Melempar pesan generik seperti "Terjadi kesalahan" tanpa memberikan konteks yang berguna, membuat debugging menjadi sulit.
throw new Error("Something went wrong."); // Not very helpfulPenghindaran: Pastikan pesan kesalahan deskriptif dan sertakan data yang relevan (misalnya, nilai parameter, jalur file, ID). Kelas kesalahan kustom dengan properti spesifik sangat baik untuk ini.
throw new DatabaseConnectionError("Failed to connect to DB", "users_db", "mongodb://localhost:27017"); - Tidak Membedakan Antara Kesalahan yang Menghadap Pengguna dan Internal:
Jebakan: Menampilkan pesan kesalahan teknis mentah (misalnya, jejak tumpukan, kesalahan kueri database) langsung kepada pengguna akhir.
// Bad: Exposing internal details to the user catch (error: unknown) { if (error instanceof Error) { res.status(500).send(`<h1>Server Error</h1><p>${error.stack}</p>`); } }Penghindaran: Memusatkan penanganan kesalahan untuk mencegat kesalahan internal dan menerjemahkannya menjadi pesan yang ramah pengguna dan terlokalisasi. Catat detail teknis hanya untuk pengembang.
// Good: User-friendly message for client, detailed log for developers catch (error: unknown) { // ... logging for developers ... res.status(500).send("<h1>We're sorry!</h1><p>An unexpected error occurred. Please try again later.</p>"); } - Memodifikasi Objek Kesalahan:
Jebakan: Memodifikasi objek
errorsecara langsung di dalam blok `catch`, terutama jika kemudian dilemparkan kembali atau diteruskan ke penangan lain. Ini dapat menyebabkan efek samping yang tidak terduga atau hilangnya konteks kesalahan asli.Penghindaran: Jika Anda perlu memperkaya kesalahan, buat objek kesalahan baru yang membungkus yang asli, atau teruskan konteks tambahan secara terpisah. Kesalahan asli harus tetap tidak dapat diubah untuk tujuan debugging.
Dengan secara sadar menghindari jebakan umum ini, penanganan kesalahan TypeScript Anda akan menjadi lebih tangguh, transparan, dan pada akhirnya berkontribusi pada aplikasi yang lebih stabil dan ramah pengguna.
Kesimpulan
Penanganan kesalahan yang efektif adalah landasan pengembangan perangkat lunak profesional, dan TypeScript mengangkat disiplin penting ini ke ketinggian baru. Dengan merangkul pola penanganan kesalahan yang aman tipe, pengembang dapat beralih dari perbaikan bug reaktif ke desain sistem proaktif, membangun aplikasi yang secara inheren lebih tangguh, dapat diprediksi, dan mudah dipelihara.
Kita telah menjelajahi beberapa pola yang ampuh:
- Pemeriksaan Tipe Runtime: Dengan aman mempersempit kesalahan
unknowndi blokcatchmenggunakaninstanceof Errordan type guard kustom untuk memastikan akses yang dapat diprediksi ke properti kesalahan. - Kelas Kesalahan Kustom: Merancang hierarki tipe kesalahan semantik yang memperluas
Errordasar, menyediakan informasi kontekstual yang kaya dan memfasilitasi penanganan yang presisi dengan pemeriksaaninstanceof. - Pola Monad Result/Either: Pendekatan fungsional alternatif yang secara eksplisit mengkodekan keberhasilan dan kegagalan dalam tipe pengembalian fungsi, memaksa pemanggil untuk menangani kedua hasil dan mengurangi ketergantungan pada mekanisme pengecualian tradisional.
- Penanganan Kesalahan Terpusat: Mengimplementasikan penangan kesalahan global (misalnya, middleware, error boundary) untuk memastikan logging, pemantauan, dan umpan balik pengguna yang konsisten di seluruh aplikasi, membedakan antara kesalahan operasional dan kesalahan pemrogram.
Setiap pola menawarkan keuntungan unik, dan pilihan optimal sering kali bergantung pada konteks spesifik, gaya arsitektur, dan preferensi tim. Namun, benang merah yang melintasi semua pendekatan ini adalah komitmen terhadap keamanan tipe. Sistem tipe TypeScript yang ketat bertindak sebagai penjaga yang kuat, memandu Anda menuju kontrak kesalahan yang lebih tangguh dan membantu Anda menangkap potensi masalah pada waktu kompilasi alih-alih pada waktu runtime.
Mengadopsi strategi ini adalah investasi yang memberikan dividen dalam stabilitas aplikasi, produktivitas pengembang, dan kepuasan pengguna secara keseluruhan, terutama saat beroperasi di lanskap perangkat lunak global yang dinamis dan beragam. Mulailah mengintegrasikan pola penanganan kesalahan yang aman tipe ini ke dalam proyek TypeScript Anda hari ini, dan bangun aplikasi yang berdiri kokoh melawan tantangan yang tak terhindarkan di dunia digital.